#include "gmatlab.h"

extern GQuark matlabWrapperKey;

/*
 * g_matlab_init:
 *
 * Initializes glib-matlab bindings.  Install matlab log reporting.
 * Must be called when the libglib-matlab is loaded.
 */
void g_matlab_init()
{
  g_type_init();
  matlabWrapperKey = g_quark_from_string("matlab-wrapper");
  g_log_set_default_handler(g_matlab_log_func, NULL);
/*
  g_log_set_handler(domain,
                    G_LOG_FLAG_FATAL | G_LOG_LEVEL_ERROR |
                    G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING,
                    g_matlab_log_func, NULL);
*/
}

/*
 * g_irepository_matlab_find_symbol:
 * @ns: the namespace to search
 * @name: the symbol to find
 *
 * Attempts to find the symbol @name in @ns.  If found, create the
 * appropriate MATLAB wrapper for the symbol.  Throws a MATLAB error
 * if the symbol cannot be found.
 *
 * Returns: a MATLAB wrapper for @name
 */
mxArray *g_irepository_matlab_find_symbol(gchar *ns, gchar *name)
{
  mxArray *ret, *args[4];
  GIBaseInfo *info;
/*
  GITypeInfo retType;
  GIArgInfo argInfo;
  gboolean isvoid;
  guint i, num;
*/
  
  info = g_irepository_find_by_name(NULL, ns, name);
  if (info == NULL)
    g_matlab_error("gmatlab:notFound", "Cannot find symbol %s in GObject-Introspection namespace %s", name, ns);

  /* Matlab will assume ownership of info.
   * Since it isn't likely the following will fail, we won't bother with 
   * escape cleanup to allow re-entrance.  */

  switch (g_base_info_get_type(info))
  {
    case GI_INFO_TYPE_FUNCTION:
/*
      g_callable_info_load_return_type(info, &retType);
      isvoid = (g_type_info_get_tag(&retType) == GI_TYPE_TAG_VOID);
      num = g_callable_info_get_n_args(info);
      for (i=0; isvoid && i < num; i++)
      {
        g_callable_info_load_arg(info, i, &argInfo);
        isvoid = (g_arg_info_get_direction(&argInfo) == GI_DIRECTION_IN); 
      }
      // Return (giInfo, isvoid) pair
*/
      args[0] = mxCreateNumericMatrix(1, 1, MX_GPOINTER, mxREAL);
      ((gpointer*)mxGetData(args[0]))[0] = info;
      args[1] = mxCreateNumericMatrix(1, 1, mxLOGICAL_CLASS, mxREAL);
      ((mxLogical*)mxGetData(args[1]))[0] = FALSE;
      args[2] = g_matlab_sprintf("FUNCTION NAME");
      mexCallMATLAB(1, &ret, 3, args, "glib.GIFunction");
      mxDestroyArray(args[0]);
      mxDestroyArray(args[1]);
      mxDestroyArray(args[2]);
      return ret;
    
    case GI_INFO_TYPE_BOXED:
    case GI_INFO_TYPE_STRUCT:
    case GI_INFO_TYPE_OBJECT:
      args[0] = mxCreateNumericMatrix(1, 1, MX_GLONG, mxREAL);      // G_TYPE_GTYPE
      args[1] = mxCreateNumericMatrix(1, 1, MX_GPOINTER, mxREAL);   // gtype
      args[2] = mxCreateNumericMatrix(1, 1, MX_GPOINTER, mxREAL);   // GTypeInfo
      args[3] = g_matlab_sprintf("%s.%s", g_base_info_get_namespace(info),
                                 g_base_info_get_name(info));
      ((glong*)mxGetData(args[0]))[0] = G_TYPE_GTYPE;
      ((gpointer*)mxGetData(args[1]))[0] =
                         (gpointer)g_registered_type_info_get_g_type(info);
      ((gpointer*)mxGetData(args[2]))[0] = info;
      TRACE("g_irepository_matlab_find_symbol: wrapping %s (%d, %ld, %ld)", g_base_info_get_type(info) == GI_INFO_TYPE_OBJECT ? "class" : "interface", G_TYPE_GTYPE, ((gpointer*)mxGetData(args[1]))[0], info);
      mexCallMATLAB(1, &ret, 4, args,
                    g_base_info_get_type(info) == GI_INFO_TYPE_OBJECT
                    ? "glib.GClass" : "glib.GInterface");
      mxDestroyArray(args[0]);
      mxDestroyArray(args[1]);
      mxDestroyArray(args[2]);
      mxDestroyArray(args[3]);
      return ret;
    
    default:
      g_matlab_error("gmatlab:unsupported", "Unsupported GI Info type %d", g_base_info_get_type(info));
  }

  return NULL;    // Not reached
}

/*
 * g_irepository_matlab_find_method:
 * @type: the #GIBaseInfo to search
 * @name: the method to find
 *
 * Attempts to find the method @name in @type.
 * If @type is a #GIObjectInfo, searches through all parents as well.
 *
 * Returns: the (#GIFunctionInfo, isvoid) pair for the method, 
 * or %NULL if not found
 */
mxArray *g_irepository_matlab_find_method(GIBaseInfo *type, gchar *name)
{
  GIFunctionInfo *meth;
  GIObjectInfo *otype;
  guint i, num;

  g_return_val_if_fail(name != NULL, NULL);
  
  switch (g_base_info_get_type(type))
  {
    case GI_INFO_TYPE_BOXED:
    case GI_INFO_TYPE_STRUCT:
      meth = g_struct_info_find_method((GIStructInfo*)type, name);
      break;
      
    case GI_INFO_TYPE_OBJECT:
      otype = (GIObjectInfo*)type;
      g_return_val_if_fail(G_TYPE_IS_OBJECT(g_registered_type_info_get_g_type(otype)), NULL);
      g_base_info_ref(type);
      g_matlab_cleanup_add((GHookFunc)g_base_info_unref, type);
      while (otype)
      {
//        TRACE("Checking %s for method %s", g_base_info_get_name(otype), name);
        // Check the class itself
        meth = g_object_info_find_method(otype, name);
        if (meth) break;
        // Try each of the class's interfaces
        // TODO: Is this actually necessary?
        num = g_object_info_get_n_interfaces(otype);
        for (i=0; i < num; i++)
        {
          GIInterfaceInfo *interface = g_object_info_get_interface(otype, i);
          g_matlab_cleanup_add((GHookFunc)g_base_info_unref, interface);
          meth = g_interface_info_find_method(interface, name);
          if (meth) break;
        }
        if (g_registered_type_info_get_g_type(otype) == G_TYPE_OBJECT)
          break;
        // Try parent
        otype = g_object_info_get_parent(otype);
        g_matlab_cleanup_add((GHookFunc)g_base_info_unref, otype);
      }
      break;
    
    default:
      g_matlab_error("gmatlab:unsupported", "g_irepository_matlab_find_method: Unsupported type %d", g_base_info_get_type(type));
  }

  if (meth)
  {
    mxArray *ret, *args[3];
/*
    GITypeInfo retType;
    GIArgInfo argInfo;
    gboolean isvoid;
    gint num, i;

    g_callable_info_load_return_type(meth, &retType);
    isvoid = (g_type_info_get_tag(&retType) == GI_TYPE_TAG_VOID);
    num = g_callable_info_get_n_args(meth);
    for (i=0; isvoid && i < num; i++)
    {
      g_callable_info_load_arg(meth, i, &argInfo);
      isvoid = (g_arg_info_get_direction(&argInfo) == GI_DIRECTION_IN); 
    }
 */
    args[0] = mxCreateNumericMatrix(1, 1, MX_GPOINTER, mxREAL);
    ((gpointer*)mxGetData(args[0]))[0] = meth;
    args[1] = mxCreateNumericMatrix(1, 1, mxLOGICAL_CLASS, mxREAL);
    ((mxLogical*)mxGetData(args[1]))[0] =
                   (g_function_info_get_flags(meth) & GI_FUNCTION_IS_METHOD);
    args[2] = g_matlab_sprintf("FUNCTION NAME");
    mexCallMATLAB(1, &ret, 3, args, "glib.GIFunction");
    mxDestroyArray(args[0]);
    mxDestroyArray(args[1]);
    mxDestroyArray(args[2]);
    return ret;
  }
  else g_matlab_error("gmatlab:notFound", "Cannot find method %s for %s.%s", name, g_base_info_get_namespace(type), g_base_info_get_name(type));
  
  g_return_val_if_reached(NULL);
}

/*
 * g_object_matlab_get_property:
 * @obj: a #GObject
 * @name: the property to get
 *
 * Gets the property @name from @obj.
 *
 * Returns: The property as an #mxArray
 */
mxArray * g_object_matlab_get_property(GObject *obj, gchar *name)
{
  mxArray *mxValue;
  GParamSpec *pspec;
  GValue *value = g_matlab_cleanup_add((GHookFunc)g_free, g_new0(GValue, 1));
  gpointer data;
  mwSize dim = 1;

  g_return_val_if_fail(G_IS_OBJECT(obj), NULL);
  g_return_val_if_fail(name != NULL, NULL);
  
  pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name);
  if (pspec == NULL)
    g_matlab_error("gmatlab:notFound", "g_object_matlab_get_property: Object does not have property %s", name);

  g_value_init(value, G_PARAM_SPEC_VALUE_TYPE(pspec));
  g_object_get_property(obj, name, value);
  g_matlab_cleanup_add((GHookFunc)g_value_unset, value);
  mxValue = g_value_to_mxArray(value);
  return mxValue;
}

/*
 * g_object_matlab_set_property:
 * @obj: a #GObject
 * @name: the property to set
 * @value: the value to set
 *
 * Set the property @name for @obj to @value.
 */
void g_object_matlab_set_property(GObject *obj, gchar *name, mxArray *mxValue)
{
  GParamSpec *pspec;
  GValue *value = g_matlab_cleanup_add(g_free, g_new0(GValue, 1));
  gpointer data;

  g_return_if_fail(G_IS_OBJECT(obj));
  g_return_if_fail(name != NULL);
  g_return_if_fail(mxValue != NULL);
  
  pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name);
  if (pspec == NULL)
    g_matlab_error("gmatlab:notFound", "g_object_matlab_get_property: Object does not have property %s", name);
  
  g_value_from_mxArray(value, mxValue);
  g_matlab_cleanup_add((GHookFunc)g_value_unset, value);
  g_object_set_property(obj, name, value);
}

static void dispose_array_wrapper(gpointer array)
{
  g_array_free(array, FALSE); 
}

/*
 * g_irepository_matlab_invoke:
 * @func: the #GIFunctionInfo to invoke
 * @args: 1xnin cell array with the arguments
 *
 * Invokes the function @func with arguments @args.  Attempts to cast
 * @args to correct type, if possible.
 *
 * Returns: the return value and any OUT parameters as 1xnout cell array
 * or %NULL if there are no returned values
 */
mxArray * g_irepository_matlab_invoke(GIFunctionInfo *func, mxArray *mxArgs)
{
  GError *err = NULL;
  mxArray *mxRet;
  gboolean isMethod, haveRet;
  int i, Iin, Iout, nin=0, nout=0;

  g_return_val_if_fail(GI_IS_FUNCTION_INFO(func), NULL);
  g_return_val_if_fail(mxArgs != NULL, NULL);
  g_return_val_if_fail(mxIsCell(mxArgs), NULL);
  isMethod = (g_function_info_get_flags(func) & GI_FUNCTION_IS_METHOD != 0);
  int nargs = g_callable_info_get_n_args((GICallableInfo*)func);
  if (isMethod) nargs++;

  /* Non-stack derived types (item types and interfaces) are allocated in 
   * g_arg_from_mxArray and g_arg_to_mxArray.  As it turns out, these derived
   * objects include a reference to their container, and if the containers
   * (the type infos here) are stack allocated, then the derived objects 
   * can't be accessed once *this* function exits.  But, this defeats the 
   * g_matlab_cleanup() mechanism, which won't be called until later.
   * So, we allocate space for the infos here instead of using the stack. */
  GIArgInfo *argInfo = g_new(GIArgInfo, nargs);
  g_matlab_cleanup_add(g_free, argInfo);
  GITypeInfo *argType = g_new(GITypeInfo, nargs+1);
  g_matlab_cleanup_add(g_free, argType);
  // retType is stored at the end of the arguments
  GITypeInfo *retType = argType + nargs;
  GIDirection dir[nargs+1];
  GITransfer transfer[nargs+1];
  
  // Load types and count in an out args
  if (isMethod)
  {
    nin++;
    i=1;
    dir[0] = GI_DIRECTION_IN;
    argType[0] = *(g_base_info_get_container(func));
    transfer[0] = GI_TRANSFER_NOTHING;
    // Note: GITypeInfo and GIObjectInfo are just aliases for GIBaseInfo
    // Note: argInfo[0] is undefined, but we won't reference it below.
  } else i=0;
  for (; i < nargs; i++)
  {
    g_callable_info_load_arg(func, isMethod ? i-1 : i, argInfo+i);
    g_arg_info_load_type(argInfo+i, argType+i);
    dir[i] = g_arg_info_get_direction(argInfo+i);
    transfer[i] = g_arg_info_get_ownership_transfer(argInfo +i);
    if (dir[i] == GI_DIRECTION_IN || dir[i] == GI_DIRECTION_INOUT) nin++;
    if (dir[i] == GI_DIRECTION_OUT || dir[i] == GI_DIRECTION_INOUT) nout++;
  }
  g_callable_info_load_return_type(func, retType);
  haveRet = (g_type_info_get_tag(retType) != GI_TYPE_TAG_VOID);
  dir[nargs] = GI_DIRECTION_OUT;
  transfer[nargs] = g_callable_info_get_caller_owns(func);

  if (mxGetNumberOfElements(mxArgs) != nin)
    g_matlab_error("gmatlab:numArgs", "g_irepository_matlab_invoke: Function requires %d paramters, %d given", nin, mxGetNumberOfElements(mxArgs));

  /* out_values are where the values are actually stores, but the arguments
   * themselves are *pointers* to those values. */
  GArgument args_in[nin], args_out[nout], out_values[nout+1];
  GArgument *retVal = out_values+nout;
  int posIn[nargs+1], posOut[nargs+1], outArgPos[nout];
  
  // Identify argument locations
  for (i=Iin=Iout=0; i <= nargs; i++)
  {
    switch(dir[i])
    {
      case GI_DIRECTION_IN:
        posIn[i] = Iin;
        posOut[i] = -1;
        Iin++;
        break;
      case GI_DIRECTION_OUT:
        posIn[i] = -1;
        posOut[i] = Iout;
        outArgPos[Iout] = i;
        Iout++;
        break;
      case GI_DIRECTION_INOUT:
        posIn[i] = Iin;
        posOut[i] = Iout;
        outArgPos[Iout] = i;
        Iin++;
        Iout++;
        break;
    }
  }

  // Prepare arguments
  for(i=0; i < nargs; i++)
    switch (dir[i])
    {
      case GI_DIRECTION_IN:
        // TODO: Check lengths of input arrays
        g_arg_from_mxArray(args_in+posIn[i], argType+i,
                           mxGetCell(mxArgs, posIn[i]), transfer[i], i+1);
        break;
      case GI_DIRECTION_INOUT:
        args_in[posIn[i]].v_pointer = out_values+posOut[i];
        args_out[posOut[i]].v_pointer = out_values+posOut[i];
        g_arg_from_mxArray(out_values+posOut[i], argType+i,
                           mxGetCell(mxArgs, posIn[i]), transfer[i], i+1);
        break;
      case GI_DIRECTION_OUT:
        args_out[posOut[i]].v_pointer = out_values+posOut[i];
        if (g_arg_info_is_caller_allocates(argInfo+i) && 
            g_type_info_get_tag(argType+i) == GI_TYPE_TAG_INTERFACE)
        {
          GITypeInfo *interface = g_type_info_get_interface(argType+i);
          g_matlab_cleanup_add((GHookFunc)g_base_info_unref, interface);
          if (GI_IS_STRUCT_INFO(interface))
          {
            gsize size = g_struct_info_get_size(interface);
            gpointer data = g_malloc0(size);
            /* In the case of a caller-allocates struct, the value and the
             * argument are both a pointer to the struct. */
            args_out[posOut[i]].v_pointer = data;
            out_values[posOut[i]].v_pointer = data;
            g_matlab_escape_add(g_free, data);
            transfer[i] = GI_TRANSFER_EVERYTHING;
            // TODO: Could this be freed twice in escape?
          }
        }
        break;
    }

  // Make the call
//  TRACE("g_irepository_matlab_invoke: Invoking %ld.", func);
  if (!g_function_info_invoke(func, args_in, nin, args_out, nout, retVal, &err))
    g_matlab_error_with_error("gmatlab:invoke", err, "g_irepository_matlab_invoke: Error invoking function");
//  TRACE("g_irepository_matlab_invoke: Invocation complete.");

//  if (!haveRet && nout == 0) return NULL;  // Nothing to return, so all done.
  
  // Convert C arrays to GArray
  for (i=0; i <= nargs; i++)   // recall retVal is in last arg
    if (dir[i] == GI_DIRECTION_OUT || dir[i] == GI_DIRECTION_INOUT)
    {
      if (g_type_info_get_tag(argType+i) == GI_TYPE_TAG_ARRAY &&
          g_type_info_get_array_type(argType+i) == GI_ARRAY_TYPE_C)
      {
        GArray *array;
        guint elementSize;
        int len, iout = posOut[i];
        if (g_type_info_is_zero_terminated(argType+i))
          len = g_strv_length(out_values[iout].v_pointer);
        else if ((len = g_type_info_get_array_fixed_size(argType+i)) < 0)
        {
          int where;
          if ((where = g_type_info_get_array_length(argType+i)) < 0)
            g_error("gmatlab:systemError", "g_irepository_matlab_invoke: Unable to determine length of C array");
          GArgument *tmp;
          g_return_val_if_fail(where < nargs, NULL);
          if (dir[where] == GI_DIRECTION_IN)
            tmp = args_in + posIn[where];
          else
            tmp = out_values + posOut[where];
          switch (g_type_info_get_tag(argType+where))
          {
            case GI_TYPE_TAG_INT8:
            case GI_TYPE_TAG_UINT8:   len = tmp->v_uint8;  break;
            case GI_TYPE_TAG_INT16:
            case GI_TYPE_TAG_UINT16:  len = tmp->v_uint16; break;
            case GI_TYPE_TAG_INT32:
            case GI_TYPE_TAG_UINT32:  len = tmp->v_uint32; break;
            case GI_TYPE_TAG_INT64:
            case GI_TYPE_TAG_UINT64:  len = tmp->v_uint64; break;
            default:
              g_matlab_error("gmatlab:systemError", "g_irepository_matlab_invoke: Unable to read length of C array from param %d of type %s (%d)", where, g_type_tag_to_string(g_type_info_get_tag(argType+where)), g_type_info_get_tag(argType+where));
          }
        } // determine array length
        
        switch (g_type_info_get_tag(g_matlab_cleanup_add(
                (GHookFunc)g_base_info_unref,
                g_type_info_get_param_type(argType+i, 0))))
        {
          case GI_TYPE_TAG_BOOLEAN: elementSize = sizeof(gboolean); break;
          case GI_TYPE_TAG_INT8:
          case GI_TYPE_TAG_UINT8:   elementSize = 1;                break;
          case GI_TYPE_TAG_INT16:
          case GI_TYPE_TAG_UINT16:  elementSize = 2;                break;
          case GI_TYPE_TAG_INT32:
          case GI_TYPE_TAG_UINT32:  elementSize = 4;                break;
          case GI_TYPE_TAG_INT64:
          case GI_TYPE_TAG_UINT64:  elementSize = 8;                break;
          case GI_TYPE_TAG_FLOAT:   elementSize = sizeof(gfloat);   break;
          case GI_TYPE_TAG_DOUBLE:  elementSize = sizeof(gdouble);  break;
          default:                  elementSize = GLIB_SIZEOF_VOID_P;
        }
        
        array = g_array_new(g_type_info_is_zero_terminated(argType+i),
                            FALSE, elementSize);
        array->data = out_values[iout].v_pointer;
        array->len = len;
        out_values[iout].v_pointer = array;
        if (transfer[i] == GI_TRANSFER_NOTHING)
          g_matlab_cleanup_add(dispose_array_wrapper, array);

      } // if need to convert
    } // if out param
  
  // Process output
  if (haveRet)
  {
    mxRet = mxCreateCellMatrix(1, nout+1);
    mxSetCell(mxRet, 0,
              g_arg_to_mxArray(retVal, retType, transfer[nargs]));
  } else mxRet = mxCreateCellMatrix(1, nout);
  for(i=0; i < nout; i++)
    mxSetCell(mxRet, haveRet ? i+1 : i,
              g_arg_to_mxArray(out_values+i, argType+outArgPos[i],
                               transfer[outArgPos[i]]));

  return mxRet;
}

